<!DOCTYPE html>
<title>@scroll-timeline element offset invalidation</title>
<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#typedef-element-offset">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<style>
  #scroller {
    overflow: scroll;
    width: 100px;
    height: 100px;
  }
  #scroller > div {
    height: 50px;
  }
  @keyframes expand {
    from { width: 100px; }
    to { width: 200px; }
  }
  @scroll-timeline timeline {
    source: selector(#scroller);
    time-range: 1e10s;
    start: selector(#offset1) end;
    end: selector(#offset2) end;
  }
  #element {
    width: 0px;
    height: 20px;
    animation: expand 1e10s linear;
    animation-timeline: timeline;
  }
  /* Ensure stable expectations if feature is not supported */
  @supports not (animation-timeline:foo) {
    #element { animation-play-state: paused; }
  }
</style>
<div id=scroller></div>
<div id=element></div>
<p class=sibling1></p>
<p class=sibling2></p>
<script>

  function setup() {
    while (scroller.firstChild)
      scroller.firstChild.remove();
    for (let i = 0; i < 10; i++)
      scroller.append(document.createElement('div'));
  }

  // The contents of the scroller look like this:
  //
  //  +-------+
  //  | 50px  | div (0)
  //  +-------+
  //  +-------+
  //  | 50px  | div (1)
  //  +-------+
  //  +-------+
  //  | 50px  | div (2)
  //  +-------+
  //  +-------+
  //  | 50px  | div (3)
  //  +-------+
  //  +-------+
  //  | 50px  | div (4)
  //  +-------+
  //  +-------+
  //  | 50px  | div (5)
  //  +-------+
  //  +-------+
  //  | 50px  | div (6)
  //  +-------+
  //  +-------+
  //  | 50px  | div (7)
  //  +-------+
  //  +-------+
  //  | 50px  | div (8)
  //  +-------+
  //  +-------+
  //  | 50px  | div (9)
  //  +-------+
  //
  // The height of the scrollport is 100px.

  function invalidation_test(func, description) {
    promise_test(async (t) => {
      setup();
      await func();
    }, description);
  }

  function remove(id) {
    let old_element = document.getElementById(id);
    if (old_element)
      old_element.removeAttribute('id');
  }

  function reassign(id, element) {
    remove(id);
    element.setAttribute('id', id);
  }

  async function assert_element_width_at_scroll(expected_width, scroll) {
    scroller.scrollTop = scroll;
    await waitForNextFrame();
    assert_equals(getComputedStyle(element).width, expected_width);
  }

  invalidation_test(async () => {
    await assert_element_width_at_scroll('0px', 0);
  }, 'Offsets missing');

  invalidation_test(async () => {
    // [50, 150]
    reassign('offset1', scroller.children[3]);
    reassign('offset2', scroller.children[5]);
    await assert_element_width_at_scroll('150px', 100);

    // [100, 150]
    reassign('offset1', scroller.children[4]);
    await assert_element_width_at_scroll('100px', 100);
  }, 'Change first offset');

  invalidation_test(async () => {
    // [50, 150]
    reassign('offset1', scroller.children[3]);
    reassign('offset2', scroller.children[5]);
    await assert_element_width_at_scroll('150px', 100);

    // [50, 250]
    reassign('offset2', scroller.children[7]);
    await assert_element_width_at_scroll('125px', 100);
  }, 'Change second offset');

  invalidation_test(async () => {
    // [50, 250]
    reassign('offset1', scroller.children[3]);
    reassign('offset2', scroller.children[7]);
    await assert_element_width_at_scroll('125px', 100);

    // [0, 200]
    reassign('offset1', scroller.children[2]);
    reassign('offset2', scroller.children[4]);
    await assert_element_width_at_scroll('150px', 50);
  }, 'Change both offsets');

  invalidation_test(async () => {
    // [50, 150]
    reassign('offset1', scroller.children[3]);
    reassign('offset2', scroller.children[5]);
    await assert_element_width_at_scroll('150px', 100);

    remove('offset1');
    await assert_element_width_at_scroll('0px', 0);
  }, 'Remove first offset');

  invalidation_test(async () => {
    // [50, 150]
    reassign('offset1', scroller.children[3]);
    reassign('offset2', scroller.children[5]);
    await assert_element_width_at_scroll('150px', 100);

    remove('offset2');
    await assert_element_width_at_scroll('0px', 0);
  }, 'Remove second offset');

  invalidation_test(async () => {
    // [50, 150]
    reassign('offset1', scroller.children[3]);
    reassign('offset2', scroller.children[5]);
    await assert_element_width_at_scroll('150px', 100);

    remove('offset1');
    remove('offset2');
    await assert_element_width_at_scroll('0px', 0);
  }, 'Remove both offsets');

  invalidation_test(async () => {
    // [50, 150]
    reassign('offset1', scroller.children[3]);
    reassign('offset2', scroller.children[5]);
    await assert_element_width_at_scroll('150px', 100);

    reassign('offset1', document.querySelector('.sibling1'));
    await assert_element_width_at_scroll('0px', 0);
  }, 'Reassign first offset to sibling of scroller');

  invalidation_test(async () => {
    // [50, 150]
    reassign('offset1', scroller.children[3]);
    reassign('offset2', scroller.children[5]);
    await assert_element_width_at_scroll('150px', 100);

    reassign('offset2', document.querySelector('.sibling2'));
    await assert_element_width_at_scroll('0px', 0);
  }, 'Reassign second offset to sibling of scroller');

  invalidation_test(async () => {
    // [50, 150]
    reassign('offset1', scroller.children[3]);
    reassign('offset2', scroller.children[5]);
    await assert_element_width_at_scroll('150px', 100);

    reassign('offset1', document.querySelector('.sibling1'));
    reassign('offset2', document.querySelector('.sibling2'));
    await assert_element_width_at_scroll('0px', 0);
  }, 'Reassign both offsets to sibling of scroller');
</script>
